前言 在「用程式打造選股策略」這一系列文的前面幾篇,已經爬了許多基本資料下來, 但如果想要做基本面回測,還必須得從當天股價的和當時的月報、季報來計算當時的本益比、殖利率、股價淨值比,相當的麻煩。
不過我們的證交所真的是很佛心,這些資訊全部都已經幫你整理好了,甚至還可以抓到歷史資料, 位置: 台灣證券交易所-個股日本益比、殖利率及股價淨值比
不囉嗦,先把資料爬下來再說
爬取歷史每日 - 個股日本益比、殖利率及股價淨值比
觀察網站的部分這邊就省略了,就是下載CSV檔案而已,應該很容易吧!
NET Core Encoding 問題 這邊補充一些前面幾篇可能也有的問題 因為 .NET Core 為了瘦身,將不常用的編碼放到Nuget包System.Text.Encoding.CodePages
內 這會造成我們在讀取Big5編碼時出現錯誤,因此我們必須將編碼引入,如下:
1 dotnet add package System.Text.Encoding.CodePages --version 4.7.0
引用Encoding:
1 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
爬蟲部分 爬取CSV,直接讀成string型態回傳,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public async Task<string > GetCsvAsync (DateTime date ) { using (var client = _clientFactory.CreateClient()) { var response = await client.GetAsync( $"https://www.twse.com.tw/exchangeReport/BWIBBU_d?response=csv&date={date.ToString("yyyyMMdd" )} &selectType=ALL" ); var bytes = await response.Content.ReadAsByteArrayAsync(); var result = Encoding.GetEncoding(950 ).GetString(bytes); if (response.StatusCode != System.Net.HttpStatusCode.OK) throw new PlatformNotSupportedException($"目前無法爬取每日基本面資料...,{response.StatusCode} ,{result} " ); return result; } }
解析CSV資料 一樣使用 CsvHelper
套件,安裝方法可以參考: 用 C# .NET Core 爬取每月財報 | 用程式打造選股策略(4)
首先我們建立DB Model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [Table("FundamentalDaily" ) ] public class FundamentalDaily { [ExplicitKey ] public DateTime date { get ; set ; } [ExplicitKey ] [Name("證券代號" ) ] public string stock_id { get ; set ; } [Name("殖利率(%)" ) ] public decimal ? dividend_yield { get ; set ; } [Name("本益比" ) ] public decimal ? pe_ratio { get ; set ; } [Name("股價淨值比" ) ] public decimal ? price_book_ratio { get ; set ; } }
由於CSV檔案內,會有些不存在的資料會用「-」號來表示, 根據 CsvHelper官方文件 ,我們可以繼承 ClassMap 來將過濾掉一些這些不正確的值!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class FundamentalDailyMap: ClassMap<FundamentalDaily> { public FundamentalDailyMap ( ) { AutoMap(CultureInfo.InvariantCulture); Map(m => m.date).Ignore(); Map(m => m.dividend_yield).ConvertUsing(row => { var field = row.GetField("殖利率(%)" ); if (!field.ToString().Contains("-" )) return Convert.ToDecimal(field); else return null ; }); Map(m => m.pe_ratio).ConvertUsing(row => { var field = row.GetField("本益比" ); if (!field.ToString().Contains("-" )) return Convert.ToDecimal(field); else return null ; }); Map(m => m.price_book_ratio).ConvertUsing(row => { var field = row.GetField("股價淨值比" ); if (!field.ToString().Contains("-" )) return Convert.ToDecimal(field); else return null ; }); } }
然後我們將剛剛的string傳入, 並且直接讀成IEnumerable<FundamentalDaily>
, 這樣就可以等等就可以直接存進資料庫!
不過這份CSV的頭尾都有一些說明資訊, 所以這裡一行一行的讀取,並且用 Regex
將不合法的資料過濾掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public IEnumerable<FundamentalDaily> ReadCsv (string data ) { using (var reader = new StringReader(data)) using (var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture)) { csvReader.Configuration.RegisterClassMap<FundamentalDailyMap>(); while (csvReader.Read()) { FundamentalDaily fundamentalDaily = null ; try { if (!Regex.IsMatch(csvReader.Context.RawRecord,".*,\r\n" )) continue ; fundamentalDaily = csvReader.GetRecord<FundamentalDaily>(); } catch (CsvHelper.TypeConversion.TypeConverterException ex) { _logger.LogDebug(ex.Message); } catch (Exception ex) { _logger.LogWarning(ex.Message); } if (fundamentalDaily != null ) yield return fundamentalDaily; } } }
存入資料庫 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class FundamentalDailyRepository { private readonly SqlConnection _conn; private readonly ILogger<FundamentalDailyRepository> _logger; public FundamentalDailyRepository (ILogger<FundamentalDailyRepository> logger, SqlConnection conn ) { _logger = logger; _conn = conn; } public void Insert (IEnumerable<FundamentalDaily> fundamentalDailyList ) { try { using (var scope = new TransactionScope()) { foreach (var fundamentalDaily in fundamentalDailyList) { _conn.Insert(fundamentalDaily); } scope.Complete(); } } catch (Exception ex) { _logger.LogError(ex.Message); } } public IEnumerable<FundamentalDaily> GetByDate (DateTime date ) { return _conn.Query<FundamentalDaily>( "select * from FundamentalDaily where date=@date" , new { date } ); } public bool IsExist (DateTime date ) { return _conn.ExecuteScalar<bool >( "select count(1) from FundamentalDaily where date=@date" , new { date } ); } }
存入整理的過程這裡就不寫了,反正就只是呼叫Insert
方法而已
選股 & 簡易回測 接著設計一個選股方法,方便以後篩選使用
首先,我們知道 本益比、股價淨值比 越小越好, 殖利率 => 越大越好
另外,為了避免傳入的日期是假日造成沒有資料,所以只要沒有資料,就自動將日期往前移一天,直到有資料為止
程式如下:
1 2 3 4 5 6 7 8 9 10 public IEnumerable<FundamentalDaily> GetFundamentalDailyList (DateTime date, int pe_ratio, int price_book_ratio, int dividend_yield ) { IEnumerable<FundamentalDaily> fundamentalDailyList = null ; for (int i=0 ; fundamentalDailyList == null || fundamentalDailyList.Count() == 0 ; i++) fundamentalDailyList = _fundamentalDailyRepository.GetByDate(date.AddDays(-i)); return fundamentalDailyList.Where(fundamentalDaily => fundamentalDaily.pe_ratio < pe_ratio && fundamentalDaily.price_book_ratio < price_book_ratio && fundamentalDaily.dividend_yield > dividend_yield); }
簡易回測:
首先根據剛剛的方法,篩選出這些條件:
本益比 < 15
股價淨值比 < 2
殖利率 > 4
1 GetFundamentalDailyList(new DateTime(year, month, day), 15 , 2 , 4 );
由於這樣資料還是太多,所以我額外篩選了一些條件如下:
月營收 > 前月營收
月營收 > 前年同月營收
EPS > 前季EPS
股價 > 10
股價 < 50
測試看看,每年1月1號,選出一批股票,到年底結算報酬率 結果:
年度
平均年化報酬率
篩選出的股票
2016
19.57%
1229,1442,1513,1730,2062,2107,2347,2359,2414,2433,2459,2468,2483,2488,2527,2535,2542,3005,3010,3022,3032,4720,4999,5471,6136,6192,6213,6449,9911,9924,9925,9933
2017
NaN
無符合條件
2018
-1.95%
1615, 1712, 2024, 2034, 2414, 2471, 2493, 3021, 3028, 4722, 6112, 6196, 6201, 8103, 8163, 8210, 9924
2019
20.22%
1104, 1452, 1530, 1710, 1737, 2006, 2108, 2340, 2356, 2433, 2480, 2488, 2511, 2542, 2904, 3003, 3029, 3231, 4155, 4532, 5522, 6112, 6184, 6582, 8215, 8497, 9924, 9945, 9946,
整體來看,報酬率都還算不錯, 雖然2018年是負的,但回顧大盤,當年是因為10月份有一個大波段的崩盤 (印象中記得是中美貿易戰吧) 在崩這麼慘的情況下平均報酬率也才僅僅 -1.95% 算是相當厲害了
結語 回測結果證明了,基本面選出來的股票,報酬率算相當不錯, 實際操作上雖然還是需要一定的資金,不過已經將股價壓到50元以下的情況,應該有機會實際應用才對..
最近還想把這些資料做成網站的形式呈現,不曉得能不能弄出點什麼來…QQ
↓↓↓ 如果喜歡我的文章,可以幫我按個Like! ↓↓↓
>> 或者,請我喝杯咖啡,這樣我會更有動力唷! <<<
街口支付
街口帳號: 901061546